/**
* Get more info at : www.jrebirth.org .
* Copyright JRebirth.org © 2011-2013
* Contact : sebastien.bordes@jrebirth.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jrebirth.af.core.ui;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import javafx.animation.Animation;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.scene.Node;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextAreaBuilder;
import javafx.scene.layout.Pane;
import javafx.scene.layout.PaneBuilder;
import org.jrebirth.af.api.exception.CoreException;
import org.jrebirth.af.api.facade.JRebirthEventType;
import org.jrebirth.af.api.log.JRLogger;
import org.jrebirth.af.api.ui.Controller;
import org.jrebirth.af.api.ui.Model;
import org.jrebirth.af.api.ui.NullController;
import org.jrebirth.af.api.ui.View;
import org.jrebirth.af.api.ui.annotation.AutoHandler;
import org.jrebirth.af.api.ui.annotation.AutoHandler.CallbackObject;
import org.jrebirth.af.api.ui.annotation.OnFinished;
import org.jrebirth.af.api.ui.annotation.RootNodeClass;
import org.jrebirth.af.api.ui.annotation.RootNodeId;
import org.jrebirth.af.api.ui.annotation.type.EnumEventType;
import org.jrebirth.af.core.log.JRLoggerFactory;
import org.jrebirth.af.core.ui.handler.AnnotationEventHandler;
import org.jrebirth.af.core.util.ClassUtility;
/**
*
* The class <strong>AbstractView</strong>.
*
* Base implementation of the view.
*
* @author Sébastien Bordes
*
* @param <M> The class type of the model of the view
* @param <N> Any object that is a JavaFx2 Node
* @param <C> The class type of the controller of the view
*/
public abstract class AbstractView<M extends Model, N extends Node, C extends Controller<?, ?>> implements View<M, N, C> {
/** The class logger. */
private static final JRLogger LOGGER = JRLoggerFactory.getLogger(AbstractView.class);
/** The base name of all JRebirth Annotation. */
private static final String BASE_ANNOTATION_NAME = "org.jrebirth.af.api.ui.annotation.On";
/** The view model. */
private final transient M model;
/** The view controller. */
private transient C controller;
/** The root node of this view. */
private transient N rootNode;
/** The error node used if an error occurred. */
private transient Pane errorNode;
/** The callback object to use for annotation event handler. */
private Object callbackObject;
/**
* Default Constructor.
*
* @param model the dedicated view model
*/
public AbstractView(final M model) {
// Attach the view model
this.model = model;
// Track this view creation
getModel().getLocalFacade().getGlobalFacade().trackEvent(JRebirthEventType.CREATE_VIEW, getModel().getClass(), this.getClass());
try {
// Build the root node of the view
this.rootNode = buildRootNode();
// Manage components controller
buildController();
} catch (final CoreException ce) {
this.controller = null;
this.rootNode = null;
LOGGER.log(UIMessages.CREATION_FAILURE, ce, this.getClass().getName());
buildErrorNode(ce);
}
}
/**
* Build the errorNode to display the error taht occured.
*
* @param ce the CoreException to display
*/
private void buildErrorNode(final CoreException ce) {
final TextArea ta = TextAreaBuilder.create()
.text(ce.getMessage())
.build();
this.errorNode = PaneBuilder.create().children(ta).build();
}
/**
* {@inheritDoc}
*/
@Override
public void prepare() throws CoreException {
// Initialize view components
initInternalView();
// Activate the controller to listen all components (this+children)
getController().activate();
// Process class annotation
processViewAnnotation();
// Process field annotation to attach event handler
processFields();
}
/**
* Process view annotation.
*
* This will define if callback action will the view itself or its dedicated controller
*/
private void processViewAnnotation() {
// Find the AutoHandler annotation if any because it's optional
final AutoHandler ah = ClassUtility.getLastClassAnnotation(this.getClass(), AutoHandler.class);
if (ah != null && ah.value() == CallbackObject.View) {
this.callbackObject = this;
} else {
// by default use the controller object as callback object
this.callbackObject = this.getController();
}
// Find the RootNodeId annotation
final RootNodeId rni = ClassUtility.getLastClassAnnotation(this.getClass(), RootNodeId.class);
if (rni != null) {
getRootNode().setId(rni.value().isEmpty() ? this.getClass().getSimpleName() : rni.value());
}
// Find the RootNodeClass annotation
final RootNodeClass rnc = ClassUtility.getLastClassAnnotation(this.getClass(), RootNodeClass.class);
if (rnc != null && rnc.value().length > 0) {
for (final String styleClass : rnc.value()) {
if (styleClass != null && !styleClass.isEmpty()) {
getRootNode().getStyleClass().add(styleClass);
}
}
}
// Process Event Handler Annotation
// For each View class annotation we will attach an event handler to the root node
for (final Annotation a : this.getClass().getAnnotations()) {
// Manage only JRebirth OnXxxxx annotations
if (a.annotationType().getName().startsWith(BASE_ANNOTATION_NAME)) {
try {
// Process the annotation if the node is not null
if (getRootNode() != null && getController() instanceof AbstractController) {
addHandler(getRootNode(), a);
}
} catch (IllegalArgumentException | CoreException e) {
LOGGER.log(UIMessages.VIEW_ANNO_PROCESSING_FAILURE, e, this.getClass().getName());
}
}
}
}
/**
* Process all fields' annotations to auto-link them with event handler.
*
* @throws CoreException if annotation processing fails
*/
private void processFields() throws CoreException {
final Class<?> currentClass = this.getClass();
// Parse view properties
for (final Field f : currentClass.getDeclaredFields()) {
// Only Node and Animation properties are eligible
if (Node.class.isAssignableFrom(f.getType()) || Animation.class.isAssignableFrom(f.getType())) {
// If a property was private, it must set to accessible = false after processing action
boolean needToHide = false;
// For private properties, set them accessible temporary
if (!f.isAccessible()) {
f.setAccessible(true);
needToHide = true;
}
// Process all existing annotation for the current field
processAnnotations(f);
// Reset the property visibility
if (needToHide && f.isAccessible()) {
f.setAccessible(false);
}
}
}
}
/**
* Process all OnXxxx Annotation to attach event handler on this field.
*
* @param property the field to analyze
*
* @throws CoreException if annotation processing fails
*/
private void processAnnotations(final Field property) throws CoreException {
// For each field annotation we will attach an event handler
for (final Annotation a : property.getAnnotations()) {
if (Node.class.isAssignableFrom(property.getType())) {
// Manage only JRebirth OnXxxxx annotations
if (a.annotationType().getName().startsWith(BASE_ANNOTATION_NAME)) {
try {
// Retrieve the property value
final Node node = (Node) property.get(this);
// Process the annotation if the node is not null
if (node != null && getController() instanceof AbstractController) {
addHandler(node, a);
}
} catch (IllegalArgumentException | IllegalAccessException e) {
LOGGER.log(UIMessages.NODE_ANNO_PROCESSING_FAILURE, e, this.getClass().getName(), property.getName());
}
}
// Manage only JRebirth OnFinished annotations
} else if (Animation.class.isAssignableFrom(property.getType())
&& OnFinished.class.getName().equals(a.annotationType().getName())) {
try {
// Retrieve the property value
final Animation animation = (Animation) property.get(this);
// Process the annotation if the node is not null
if (animation != null && getController() instanceof AbstractController) {
addHandler(animation, a);
}
} catch (IllegalArgumentException | IllegalAccessException e) {
LOGGER.log(UIMessages.ANIM_ANNO_PROCESSING_FAILURE, e, this.getClass().getName(), property.getName());
}
}
}
}
/**
* Add an event handler on the given node according to annotation OnXxxxx.
*
* @param node the graphical node, must be not null
* @param annotation the OnXxxx annotation
*
* @throws CoreException if an error occurred while linking the event handler
*/
private void addHandler(final Node node, final Annotation annotation) throws CoreException {
// Build the auto event handler for this annotation
final AnnotationEventHandler<Event> aeh = new AnnotationEventHandler<>(this.callbackObject, annotation);
for (final EnumEventType eet : (EnumEventType[]) ClassUtility.getAnnotationAttribute(annotation, "value")) {
node.addEventHandler(eet.eventType(), aeh);
}
}
/**
* Add an event handler on the given animation according to annotation OnFinished.
*
* @param animation the animation, must be not null
* @param annotation the OnXxxx annotation (only OnFinished is supported)
*
* @throws CoreException if an error occurred while linking the event handler
*/
private void addHandler(final Animation animation, final Annotation annotation) throws CoreException {
// Build the auto event handler for this annotation
final AnnotationEventHandler<ActionEvent> aeh = new AnnotationEventHandler<>(this.callbackObject, annotation);
// Only on event type
animation.setOnFinished(aeh);
}
/**
* {@inheritDoc}
*/
@Override
public abstract void start();
/**
* {@inheritDoc}
*/
@Override
public abstract void reload();
/**
* {@inheritDoc}
*/
@Override
public abstract void hide();
/**
* Build the root node.
*
* @return the root node of the view
*
* @throws CoreException if introspection fails
*/
@SuppressWarnings("unchecked")
protected N buildRootNode() throws CoreException {
return (N) ClassUtility.buildGenericType(this.getClass(), Node.class);
}
/**
* Build the view controller.
*
* @throws CoreException if introspection fails
*/
@SuppressWarnings("unchecked")
protected void buildController() throws CoreException {
if (!NullController.class.equals(ClassUtility.findGenericClass(this.getClass(), Controller.class))) {
// Build the controller by introspection
this.controller = (C) ClassUtility.buildGenericType(this.getClass(), Controller.class, this);
}
}
/**
* {@inheritDoc}
*/
@Override
public final M getModel() {
return this.model;
}
/**
* {@inheritDoc}
*/
@Override
public final C getController() {
return this.controller;
}
/**
* {@inheritDoc}
*/
@Override
public final N getRootNode() {
return this.rootNode;
}
/**
* {@inheritDoc}
*/
@Override
public final Pane getErrorNode() {
return this.errorNode;
}
/**
* Initialize the view.
*
* This method is a hook to manage generic code before initializing the view's node tree.
*
* You must implement the {@link #initView()} method to setup your view.
*/
protected final void initInternalView() {
// Do some generic stuff to set up the view
// Call the user method used to set up the graphical tree of nodes
initView();
}
/**
* Custom method used to initialize components.
*
* This method must be overridden by user to create its own graphical tree
*/
protected abstract void initView();
/**
* {@inheritDoc}
*/
@Override
protected void finalize() throws Throwable {
getModel().getLocalFacade().getGlobalFacade().trackEvent(JRebirthEventType.DESTROY_VIEW, null, this.getClass());
super.finalize();
}
}